﻿using Model.DocumentDB.Telemetry;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

namespace Model.DocumentDB
{
    /// <summary>Telemetry agent component.  Logs anonymous usage to a central service.</summary>
    /// <remarks>
    /// All logging is made anonymously for telemetry purposes (as opposed to spying).
    /// A user ID is computed as a hash of user-ID, domain & machine name (a hash is a one way encoding, making it
    /// impossible to retrieve the original user ID and other information).
    /// A session ID is also created in order to tie many telemetry entrances together.
    /// No user information (e.g. collection name) are sent to the central service, only usage metrics.
    /// </remarks>
    public class TelemetryAgent
    {
        private static readonly TimeSpan MIN_DELAY_BETWEEN_API_PUSH = TimeSpan.FromMinutes(5);
        private static readonly TelemetryAgent _instance = new TelemetryAgent();

        private readonly DateTime _startTime = DateTime.Now;
        private readonly string _userID = ExtractAnonymousUserID();
        private readonly string _sessionID = Guid.NewGuid().ToString();
        private readonly ReleaseVersion _releaseVersion =
            ReleaseVersion.FromCurrentVersion();
        private readonly IDictionary<Features, int> _featureCounterMap = new Dictionary<Features, int>();

        private string _snapshotApiUrl;
        private Task _pushToApiTask = null;
        private DateTime? _lastApiPush = null;

        /// <summary>Singleton instance.</summary>
        public static TelemetryAgent Instance
        {
            get { return _instance; }
        }

        /// <summary>Privator constructor:  forbid construction out of singleton.</summary>
        private TelemetryAgent() { }

        /// <summary>Starts telemetry.</summary>
        /// <param name="snapshotApiUrl">Url of the telemetry snapshot API.</param>
        public void Start(string snapshotApiUrl)
        {
            if (string.IsNullOrWhiteSpace(snapshotApiUrl))
            {
                throw new ArgumentNullException("snapshotApiUrl");
            }

            _snapshotApiUrl = snapshotApiUrl;
            PushToApi();
        }

        /// <summary>Signal the use of a feature.</summary>
        /// <param name="feature">Used feature.</param>
        public void UseFeature(Features feature)
        {
            int counter = 0;

            _featureCounterMap.TryGetValue(feature, out counter);
            _featureCounterMap[feature] = ++counter;
            PushToApiIfDelayExpires();
        }

        /// <summary>Finishes session.</summary>
        /// <returns>Asynchronous completion.</returns>
        public async Task StopAsync()
        {
            try
            {
                PushToApi();

                await _pushToApiTask;
            }
            catch { }   //  Swallows errors
        }

        private void PushToApiIfDelayExpires()
        {
            if ((_pushToApiTask == null || _pushToApiTask.IsCompleted)
                && (!_lastApiPush.HasValue || _lastApiPush.Value.Add(MIN_DELAY_BETWEEN_API_PUSH) <= DateTime.Now))
            {
                PushToApi();
            }
        }

        private void PushToApi()
        {
            if (_pushToApiTask == null || _pushToApiTask.IsCompleted)
            {
                _pushToApiTask = InvokeApiAsync();
            }
        }

        private async Task InvokeApiAsync()
        {
#if DEBUG
            //  Not to fail compilation because of lack of await
            await Task.FromResult(42);
#else
            var snapshot = GetSnapshot();
            var snapshotJson = JsonConvert.SerializeObject(snapshot, Formatting.Indented);
            var request = WebRequest.Create(_snapshotApiUrl) as HttpWebRequest;

            request.Headers.Add(HttpRequestHeader.Authorization, _userID + _sessionID);
            request.Accept = "application/json";
            request.ContentType = "application/json";
            request.Method = "PUT";

            using (var requestStream = await request.GetRequestStreamAsync())
            using (var requestWriter = new StreamWriter(requestStream))
            {
                await requestWriter.WriteAsync(snapshotJson);
            }

            using (var response = await request.GetResponseAsync() as HttpWebResponse)
            using (var responseStream = response.GetResponseStream())
            using (var responseReader = new StreamReader(responseStream))
            {
                var responsePayload = await responseReader.ReadToEndAsync();

                if (response.StatusCode != HttpStatusCode.Accepted)
                {
                    throw new InvalidOperationException(
                        "Response status code isn't 202 / Accepted",
                        new WebException(responsePayload));
                }
            }
#endif
            _lastApiPush = DateTime.Now;
        }

        private Snapshot GetSnapshot()
        {
            var snapshot = new Snapshot
            {
                UserID = _userID,
                SessionID = _sessionID,
                ReleaseVersion = _releaseVersion,
                UpTimeSeconds = (int)DateTime.Now.Subtract(_startTime).TotalSeconds,
                FeatureCounter = (from k in _featureCounterMap.Keys
                                  orderby k
                                  select new FeatureCounter { Feature = k, Count = _featureCounterMap[k] }).ToArray()
            };

            return snapshot;
        }

        private static string ExtractAnonymousUserID()
        {
            var privateID = Environment.MachineName + Environment.UserDomainName + Environment.UserName;
            var privateIDBytes = ASCIIEncoding.ASCII.GetBytes(privateID);
            var hashAlgo = new SHA1Managed();
            var hashBytes = hashAlgo.ComputeHash(privateIDBytes);
            var hashText = Convert.ToBase64String(hashBytes);

            return hashText;
        }
    }
}